/**
 * Copyright (c) <2010>, <SUNNY GUPTA>
 * All rights reserved.

 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *   * Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *   * Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *   * Neither the name of the <ORGANIZATION> nor the
 *     names of its contributors may be used to endorse or promote products
 *     derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL <SUNNY GUPTA> BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.gwtp.investmentadvisor.server.externaldataservices.hybrid;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import net.sf.jsr107cache.CacheException;

import com.google.appengine.api.memcache.Stats;
import com.google.inject.Inject;
import com.gwtp.investmentadvisor.server.core.cacheservice.CacheService;
import com.gwtp.investmentadvisor.server.core.datastore.dao.FinancialSecurityDAO;
import com.gwtp.investmentadvisor.server.core.exception.NotImplementedException;
import com.gwtp.investmentadvisor.server.core.exception.NotSupportedException;
import com.gwtp.investmentadvisor.server.core.exception.NullArgumentException;
import com.gwtp.investmentadvisor.server.entities.finance.FinancialAmount;
import com.gwtp.investmentadvisor.server.entities.finance.FinancialSecurity;
import com.gwtp.investmentadvisor.server.entities.finance.FinancialSecurityQuote;
import com.gwtp.investmentadvisor.server.entities.general.Country;
import com.gwtp.investmentadvisor.server.enums.FinancialSecurityType;
import com.gwtp.investmentadvisor.server.externaldataservices.FinancialSecurityDataProvider;
import com.gwtp.investmentadvisor.server.externaldataservices.HistoricalQuotePeriod;
import com.gwtp.investmentadvisor.server.externaldataservices.amfi.AMFIDataProvider;
import com.gwtp.investmentadvisor.server.externaldataservices.exception.DataServiceException;
import com.gwtp.investmentadvisor.server.externaldataservices.exception.InvalidDataProviderException;
import com.gwtp.investmentadvisor.server.externaldataservices.yahoo.QueryMethodType;
import com.gwtp.investmentadvisor.server.externaldataservices.yahoo.YahooFinanceDataProvider;
import com.gwtp.investmentadvisor.shared.entities.FinancialSecurityView;

/**
 * @author Sunny Gupta
 *
 */
public class CachedHybridFinancialDataService implements FinancialSecurityDataProvider {
  // Cache elements
  // For equity traded, every 30 minutes
  private static final int ET_RELATIVE_EXPIRATION_TIME_IN_SECONDS = 1800;
  // For daily traded, like MF, every 20 hours
  private static final int DT_RELATIVE_EXPIRATION_TIME_IN_SECONDS = 72000;
  
  // Error message for internal failure
  private static final String INTERNAL_FAILURE_MESSAGE = "Internal Operation Failure";

  private CacheService cache;
  private HashMap<String, FinancialSecurityDataProvider> dataProviderMap;
  private static final Logger logger = Logger.getLogger(CachedHybridFinancialDataService.class.getName());

  private final FinancialSecurityDAO financialSecurityDAO;

  @Inject
  public CachedHybridFinancialDataService(final FinancialSecurityDAO financialSecurityDAO,
      final CacheService cache) throws InvalidDataProviderException, NullArgumentException {
    // Initialize CacheService
    this.cache = cache;
    
    // Initialize DAO
    this.financialSecurityDAO  = financialSecurityDAO;
    
    // Initialize Data Providers
    initializeDataProviderMap();
  }

  private FinancialSecurityDataProvider getfinancialSecurityDataProvider(
      FinancialSecurity financialSecurity) throws NullArgumentException {
    return getfinancialSecurityDataProvider(financialSecurityDAO.getCountry(financialSecurity), 
        financialSecurity.getFinancialSecurityType());
  }
  
  private void initializeDataProviderMap() throws NullArgumentException {
    // Instantiate
    dataProviderMap = new HashMap<String, FinancialSecurityDataProvider>();
    
    Country[] supportedCountries = getSupportedCountries();
    FinancialSecurityType[] supportedTypes = {FinancialSecurityType.STOCK, 
                            FinancialSecurityType.MUTUAL_FUND};
    // Iterate and add providers
    for (Country country : supportedCountries) {
      for (FinancialSecurityType type : supportedTypes) {
        // May throw exception if this combination is not supported
        try {
          getfinancialSecurityDataProvider(country, type);
        } catch (NullArgumentException e) {
          // Problem, propagate error up
          throw e;
        } catch (Exception e) {
          // TODO: Remain silent
        }
      }
    }
  }

  private Country[] getSupportedCountries() {
    Country country = new Country();
    country.setCountryID("IN");
    Country[] supportedCountries = {country};
    
    return supportedCountries;
  }

  private FinancialSecurityDataProvider getfinancialSecurityDataProvider(
      Country country, FinancialSecurityType financialSecurityType) throws NullArgumentException {
    // Based on security type
    switch(financialSecurityType) {
    case MUTUAL_FUND:
      // Based on country
      return getCountrySpecificFinancialSecurityDataProvider(country, financialSecurityType);
    case STOCK:
      // Yahoo Provider, irrespective of country
      String key = FinancialSecurityType.STOCK.name() + "__ALL_COUNTRIES";
      Object obj = dataProviderMap.get(key);
      if (obj == null) {
        // Instantiate and add to map
        // TODO: Switch to YQL
        FinancialSecurityDataProvider provider = new YahooFinanceDataProvider(
            QueryMethodType.CSV, this.financialSecurityDAO);
        dataProviderMap.put(key, provider);
        
        return provider;
      } else {
        return (FinancialSecurityDataProvider) obj;
      }
    case ULIP:
      // Based on country
      return getCountrySpecificFinancialSecurityDataProvider(country, financialSecurityType);
    // This will never be hit. Added to workaround compile error
    default:
      // Cannot be the case
      throw new IllegalArgumentException();
    }
  }
  
  private FinancialSecurityDataProvider getCountrySpecificFinancialSecurityDataProvider(
      Country country, FinancialSecurityType financialSecurityType) {
    // Based on security type & country
    String key = financialSecurityType.name() + "__" +
            country.getCountryName();
    
    Object obj = dataProviderMap.get(key);
    if (obj == null) {
      // Instantiate and add to map
      FinancialSecurityDataProvider provider = null;
      
      // TODO: Based on country
      if ("IN".equals(country.getCountryID())) {
        // Based on type
        switch (financialSecurityType) {
        case MUTUAL_FUND:
          provider = new AMFIDataProvider();
          break;
        case ULIP:
          // Not yet supported
          throw new NotImplementedException();
        case STOCK:
          // Not needed
        default:
          break;
        }
      } else {
        // Not yet supported
        throw new NotImplementedException();
      }
      
      // Add to map
      dataProviderMap.put(key, provider);
      
      return provider;
    } else {
      return (FinancialSecurityDataProvider) obj;
    }
  }

  private int getExpirationForSecurity(FinancialSecurity financialSecurity) {
    return getExpirationForSecurityType(financialSecurity.getFinancialSecurityType());
  }

  private int getExpirationForSecurity(FinancialSecurityView financialSecurityView) {
    // Get Type
    FinancialSecurityType type = FinancialSecurityType.getValueFromDescription(financialSecurityView.getFinancialSecurityType());
    
    return getExpirationForSecurityType(type);
  }

  private int getExpirationForSecurityType(FinancialSecurityType type) {
    switch(type) {
    case MUTUAL_FUND:
      return DT_RELATIVE_EXPIRATION_TIME_IN_SECONDS;
    case STOCK:
      return ET_RELATIVE_EXPIRATION_TIME_IN_SECONDS;
    case ULIP:
      return DT_RELATIVE_EXPIRATION_TIME_IN_SECONDS;
    // This will never be hit. Added to workaround compile error
    default:
      // Cannot be the case
      throw new IllegalArgumentException();
    }
  }
  
  @Override
  public FinancialSecurityView getCurrentMarketPrice(FinancialSecurityView financialSecurityView) throws DataServiceException {
    try {
      // Generate Key:- <ReturnTypeClassname>__<TickerSymbol>__<Action>
      String key = FinancialSecurityView.class.getSimpleName() + "__" + 
              financialSecurityView.getTickerSymbol() + "__getCurrentMarketPrice";

      // Check if available in cache
      Object obj = cache.get(key);
      if (obj != null) {
        // Return value from cache
        return (FinancialSecurityView) obj;
      } else {
        // Get from financialSecurityDataProvider
        FinancialSecurity financialSecurity = new FinancialSecurity();
        financialSecurity.setTickerSymbol(financialSecurityView.getTickerSymbol());
        
        // Obtain appropriate data provider
        FinancialSecurityQuote quote = getfinancialSecurityDataProvider(financialSecurity).getCurrentMarketPrice(financialSecurity);

        // Cloning is required
        FinancialSecurityView newView = (FinancialSecurityView) financialSecurityView.clone();
        newView.setCurrentMarketPrice(quote.getPrice().toEngineeringString());

        // Add to Cache
        cache.put(key, newView, getExpirationForSecurity(financialSecurityView));

        ///Finally return
        return newView;
      }
    } catch (Exception e) {
      throw new DataServiceException(e);
    }
  }

  /**
   * This function absorbs exceptions thrown by underlying data providers, 
   * and provided a list of views if successful.
   */
  @SuppressWarnings("unchecked")
  @Override
  public List<FinancialSecurityView> searchByText(String searchText) throws DataServiceException {
    try {
      // Generate Key:- <ReturnTypeClassname>__<SearchText>
      String key = List.class.getSimpleName() + "__" + 
              FinancialSecurityView.class.getSimpleName() + "__" + 
              searchText + "__searchByText";

      // Check if available in cache
      Object obj = cache.get(key);
      if (obj != null) {
        // Return value from cache
        return (List<FinancialSecurityView>) obj;
      } else {
        // Flag to indicate if result can be cached
        boolean canResultBeCached = true;
        
        // Get from all financialSecurityDataProvider
        List<FinancialSecurityView> searchResult = new ArrayList<FinancialSecurityView>();
        for (FinancialSecurityDataProvider provider : dataProviderMap.values()) {
          // Provider may throw error, which should be caught
          List<FinancialSecurityView> result = null;
          try {
            result = provider.searchByText(searchText);
          } catch (Exception e) {
            // check if result can be cached
            canResultBeCached = canResultBeCachedOnException(e);
            
            logger.log(Level.SEVERE, INTERNAL_FAILURE_MESSAGE, e);
          } finally {
            // Ignore exception, and add if non-null
            if (result != null && result.size() > 0) {
              searchResult.addAll(result);
            }
          }
        }

        if (canResultBeCached) {
          // These results are static, they can be kept indefinitely in cache
          cache.put(key, searchResult);
        }
        
        ///Finally return
        return searchResult;
      }
    } catch (Exception e) {
      throw new DataServiceException(e);
    }
  }

  private boolean canResultBeCachedOnException(Exception e) {
    // If unknown exception, some problem and cannot be cached
    if (e instanceof DataServiceException) {
      // TODO: Check if internal error that doesn't affect cacheability
      return true;
    } else {
      return false;
    }
  }

  @Override
  public FinancialSecurityQuote getCurrentMarketPrice(
      FinancialSecurity financialSecurity) throws DataServiceException {
    try {
      // Generate Key
      String key = FinancialAmount.class.getSimpleName() + "__" + 
              financialSecurity.getTickerSymbol() + "__getCurrentMarketPrice";

      // Check if available in cache
      Object obj = cache.get(key);
      if (obj != null) {
        // Return value from cache
        return (FinancialSecurityQuote) cache.get(key);
      } else {
        // Obtain appropriate data provider
        FinancialSecurityQuote quote = getfinancialSecurityDataProvider(financialSecurity).getCurrentMarketPrice(financialSecurity);

        // Add to Cache
        cache.put(key, quote, getExpirationForSecurity(financialSecurity));

        ///Finally return
        return quote;
      }
    } catch (Exception e) {
      throw new DataServiceException(e);
    }
  }

  @Override
  public List<FinancialSecurityQuote> getCurrentMarketPriceForAllFinancialSecurities()
      throws DataServiceException, NotSupportedException {
    try {
      // Cannot be supported
      throw new NotSupportedException("Data Provider does not support this " +
          "operation, since it'd result in all of the world's possible Financial Securities " +
          "being returned. This would certainly be useless for the client, " +
          "needless to mention the huge amount of time this operation would take");
    } catch (Exception e) {
      throw new DataServiceException(e);
    }
  }

  @SuppressWarnings("unchecked")
  @Override
  public List<FinancialSecurityQuote> getHistoricalQuotes(
      FinancialSecurity financialSecurity, Calendar startDate,
      Calendar endDate, HistoricalQuotePeriod quotePeriod)
      throws DataServiceException {
    try {
      // Generate Key
      String key = List.class.getSimpleName() + "__" + 
              FinancialSecurityQuote.class.getSimpleName() + "__" + 
              financialSecurity.getTickerSymbol() + "__getHistoricalQuotes";

      // Check if available in cache
      Object obj = cache.get(key);
      if (obj != null) {
        // Return value from cache
        return (List<FinancialSecurityQuote>) cache.get(key);
      } else {
        // Obtain appropriate data provider
        List<FinancialSecurityQuote> quotes = getfinancialSecurityDataProvider(financialSecurity)
          .getHistoricalQuotes(financialSecurity, startDate, endDate, quotePeriod);

        // Add to Cache
        cache.put(key, quotes, getExpirationForSecurity(financialSecurity));

        // Finally return
        return quotes;
      }
    } catch (Exception e) {
      throw new DataServiceException(e);
    }
  }
  
  public double getCacheHitRatio() throws CacheException {
    Stats stats = cache.getMemcacheService().getStatistics();

    long hits = stats.getHitCount();
    long misses = stats.getMissCount();
    
    return  hits * 1.0 / (hits + misses);
  }
  
  /**
   * This method gives the Data Provider used for a specific country / type.
   * Note: This method is provided only for flexibility. It should not be used normally
   * @param country
   * @param financialSecurityType
   * @return
   */
  public FinancialSecurityDataProvider getUnderlyingDataProvider(Country country, 
      FinancialSecurityType financialSecurityType) {
    try {
      return getfinancialSecurityDataProvider(country, financialSecurityType);
    } catch (NullArgumentException e) {
      return null;
    }
  }
  
  /**
   * This method gives all the underlying data providers' list
   * Note: This method is provided only for flexibility. It should not be used normally
   * @return
   */
  public List<FinancialSecurityDataProvider> getAllUnderlyingDataProvider() {
    return (List<FinancialSecurityDataProvider>) dataProviderMap.values();
  }
}
